package net.lucamasini.dbprovider;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceProvider;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.SyntheticResource;
import org.h2.jdbcx.JdbcConnectionPool;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.sling.api.SlingConstants.*;

@Component(name="DBResourceProvider",															// (1)
		label="DBResourceProvider", 
		description="Sample DB Resource Provider",
		getConfigurationFactory=true)
@Service																						// (2)
@Properties({																					// (3)
	@Property(name="service.description", value="Sample DB Resource Provider"),
	@Property(name="service.vendor", value="lucamasini.net"),
	@Property(name=ResourceProvider.ROOTS, value="/content/mynamespace/products"),
	@Property(name="jdbc.url", value="jdbc:h2:~/sling-test"),
	@Property(name="jdbc.user", value="sa"),
	@Property(name="jdbc.pass", value=""),
	@Property(name=SlingConstants.PROPERTY_RESOURCE_TYPE, value="/apps/dbprovider/dbprovider.jsp")
})
public class DBResourceProvider implements ResourceProvider {

    private final static Logger log = LoggerFactory.getLogger(DBResourceProvider.class);

	private String providerRoot;
	private String providerRootPrefix;
	private String resourceType;
	
    private JdbcConnectionPool ds; 
    
    // ---------- SCR Integration

    protected void activate(BundleContext bundleContext, Map<?, ?> props) throws SQLException {

        providerRoot = props.get(ROOTS).toString();
        resourceType = props.get(PROPERTY_RESOURCE_TYPE).toString();

        this.providerRootPrefix = providerRoot.concat("/");
    	
        this.ds = JdbcConnectionPool.create(props.get("jdbc.url").toString(), props.get("jdbc.user").toString(), props.get("jdbc.pass").toString());

        log.info("DBResourceProvider ClassLoader: "+this.getClass().getClassLoader());
    	log.info("providerRoot: "+providerRoot);
        log.info("providerRootPrefix: "+providerRootPrefix);
        log.info("resourceType: "+resourceType);
        log.info("H2 connection pool: "+ds+" loaded by ClassLoader: "+ds.getClass().getClassLoader());
    }
    
    protected void deactivate() throws SQLException {
    	
		this.ds.dispose();
    	this.ds = null;
        this.providerRoot = null;
        this.providerRootPrefix = null;
        this.resourceType = null;
    }
	
	public Resource getResource(ResourceResolver paramResourceResolver,
			HttpServletRequest paramHttpServletRequest, String path) {
		log.info("entering getResource(ResourceResolver, HttpServletRequest, String) for path: "+path);

		return getResource(paramResourceResolver, path);
	}

	public Resource getResource(final ResourceResolver resourceResolver,
			final String path) {
		
		log.info("entering getResource(ResourceResolver, String) for path: "+path);
		
		if( providerRoot.equals(path) || providerRootPrefix.equals(path) ) {										// (1)
			log.info("path "+path+" matches this provider root folder: "+providerRoot);
			
			return new SyntheticResource(resourceResolver, path, "nt:folder");
		} else if ( path.startsWith(providerRootPrefix) && isNumber(path.substring(providerRootPrefix.length()))) { // (2)

			List<Resource> resources1 = runQuery("SELECT * FROM PRODUCT WHERE ID = ?", new RowMapper<Resource>() {
				public Resource mapRow(ResultSet rs) throws SQLException {
					
			    	ResultSetMetaData rsmd = rs.getMetaData();
			    	ResourceMetadata resourceMetaData = new ResourceMetadata();
			    	for(int i=1;i<=rsmd.getColumnCount();i++) {

			    		resourceMetaData.put(rsmd.getColumnName(i), rs.getObject(i));
			    	}
			    	
			    	resourceMetaData.setResolutionPath(path);
				    Resource resource = new SyntheticResource(resourceResolver, resourceMetaData, resourceType); 
					
					return resource;
				}
			}, path.substring(providerRootPrefix.length()));

			return resources1.size()==1?resources1.get(0):null;
		}
		
		return null;																								// (3)
	}

	public Iterator<Resource> listChildren(final Resource paramResource) {
		log.info("entering getResource(ResourceResolver, String) for Resource: "+paramResource.getPath());

		if( providerRoot.equals(paramResource.getPath()) ) {
			
			List<Resource> resources = runQuery("SELECT ID FROM PRODUCT", new RowMapper<Resource>() {
				public Resource mapRow(ResultSet rs) throws SQLException {
					return new SyntheticResource(paramResource.getResourceResolver(), providerRootPrefix+rs.getInt(1), resourceType);
				}
			});
			
			return resources.iterator();
		}
		
		return null;
    }
	
	private static interface RowMapper<T> {
		T mapRow(ResultSet rs) throws SQLException;
	}
	
	private <T> List<T> runQuery(String sql, RowMapper<T> rowMapper, Object...args) {
	    try {
			Connection conn = ds.getConnection();
			
			try {
				PreparedStatement ps = conn.prepareStatement(sql);
				for(int i=0; i<args.length; i++) {
					ps.setObject(i+1, args[i]);					
				}
			    ResultSet rs = ps.executeQuery();
			    
			    List<T> retVal = new ArrayList<T>();
			    while(rs.next()) {
			    	retVal.add(rowMapper.mapRow(rs));
			    }

			    rs.close();
			    ps.close();
			    
			    return retVal;
			}
			
			finally {
				conn.close();
			}
		} catch (SQLException e) {
			log.error("error getting resource for SQL: "+sql, e);
			throw new RuntimeException("error getting resource", e);
		}			
	}

	private boolean isNumber(String numberCandidate) {
		
		return Pattern.matches("^\\d*$", numberCandidate);
	}

	/*
    @SuppressWarnings("unchecked")
    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
        if (type == Servlet.class) {
            return (AdapterType) servlet; // unchecked cast
        } else if ( type == ValueMap.class ) {
            final Map<String, Object> props = new HashMap<String, Object>();
            props.put("sling:resourceType", this.getResourceType());
            props.put("sling:resourceSuperType", this.getResourceSuperType());
            props.put("servletName", this.getServletName());
            props.put("servletClass", this.servlet.getClass().getName());

            return (AdapterType) new ValueMapDecorator(props); // unchecked cast
        }

        return super.adaptTo(type);
    }
    */
	
}
